D:\a\csshw\csshw\xtask\src\changelog.rs
Line | Count | Source |
1 | | //! Changelog generation via the external `changelogging` tool. |
2 | | //! |
3 | | //! Reads the current version from `Cargo.toml`, synchronises it into |
4 | | //! `changelogging.toml`, then invokes `changelogging build --remove` to |
5 | | //! consume news fragments and append an entry to `CHANGELOG.md`. |
6 | | |
7 | | use anyhow::{Context, Result}; |
8 | | |
9 | | /// All side-effecting operations required by this module. |
10 | | /// |
11 | | /// Implement with mocks in tests to achieve zero filesystem and process |
12 | | /// side-effects. |
13 | | pub trait ChangelogSystem { |
14 | | /// Read the contents of `Cargo.toml`. |
15 | | /// |
16 | | /// # Errors |
17 | | /// |
18 | | /// Returns an error if the file cannot be read. |
19 | | fn read_cargo_toml(&self) -> Result<String>; |
20 | | |
21 | | /// Read the contents of `changelogging.toml`. |
22 | | /// |
23 | | /// # Errors |
24 | | /// |
25 | | /// Returns an error if the file cannot be read. |
26 | | fn read_changelogging_toml(&self) -> Result<String>; |
27 | | |
28 | | /// Write `content` to `changelogging.toml`. |
29 | | /// |
30 | | /// # Errors |
31 | | /// |
32 | | /// Returns an error if the write fails. |
33 | | fn write_changelogging_toml(&self, content: &str) -> Result<()>; |
34 | | |
35 | | /// Run `changelogging build --remove` to generate `CHANGELOG.md`. |
36 | | /// |
37 | | /// # Errors |
38 | | /// |
39 | | /// Returns an error if the process cannot be started or exits non-zero. |
40 | | fn run_changelogging_build(&self) -> Result<()>; |
41 | | } |
42 | | |
43 | | /// Production implementation of [`ChangelogSystem`]. |
44 | | pub struct RealSystem; |
45 | | |
46 | | #[cfg_attr(coverage_nightly, coverage(off))] |
47 | | impl ChangelogSystem for RealSystem { |
48 | | fn read_cargo_toml(&self) -> Result<String> { |
49 | | std::fs::read_to_string("Cargo.toml").context("failed to read Cargo.toml") |
50 | | } |
51 | | |
52 | | fn read_changelogging_toml(&self) -> Result<String> { |
53 | | std::fs::read_to_string("changelogging.toml").context("failed to read changelogging.toml") |
54 | | } |
55 | | |
56 | | fn write_changelogging_toml(&self, content: &str) -> Result<()> { |
57 | | std::fs::write("changelogging.toml", content).context("failed to write changelogging.toml") |
58 | | } |
59 | | |
60 | | fn run_changelogging_build(&self) -> Result<()> { |
61 | | let status = std::process::Command::new("changelogging") |
62 | | .args(["build", "--remove"]) |
63 | | .status() |
64 | | .context("failed to run `changelogging build --remove`")?; |
65 | | if !status.success() { |
66 | | anyhow::bail!("`changelogging build --remove` exited with status {status}"); |
67 | | } |
68 | | Ok(()) |
69 | | } |
70 | | } |
71 | | |
72 | | /// Extract the `[package].version` value from a `Cargo.toml` string. |
73 | | /// |
74 | | /// # Arguments |
75 | | /// |
76 | | /// * `cargo_toml_content` - Raw TOML text of `Cargo.toml`. |
77 | | /// |
78 | | /// # Returns |
79 | | /// |
80 | | /// The version string (e.g. `"0.18.1"`). |
81 | | /// |
82 | | /// # Errors |
83 | | /// |
84 | | /// Returns an error if the content cannot be parsed as TOML or the |
85 | | /// `[package].version` key is absent. |
86 | 13 | pub fn extract_version_from_cargo_toml(cargo_toml_content: &str) -> Result<String> { |
87 | 13 | let doc12 : toml_edit::Document12 = cargo_toml_content |
88 | 13 | .parse() |
89 | 13 | .context("failed to parse Cargo.toml")?1 ; |
90 | 12 | let version11 = doc |
91 | 12 | .get("package") |
92 | 12 | .and_then(|p| p.as_table()) |
93 | 12 | .and_then(|t| t.get("version")) |
94 | 12 | .and_then(|v| v11 .as_str11 ()) |
95 | 12 | .context("missing [package].version in Cargo.toml")?1 ; |
96 | 11 | Ok(version.to_owned()) |
97 | 13 | } |
98 | | |
99 | | /// Set `context.version` in a `changelogging.toml` document to `version`. |
100 | | /// |
101 | | /// All other keys and formatting are preserved via `toml_edit`. |
102 | | /// |
103 | | /// # Arguments |
104 | | /// |
105 | | /// * `changelogging_content` - Raw TOML text of `changelogging.toml`. |
106 | | /// * `version` - Version string to write. |
107 | | /// |
108 | | /// # Returns |
109 | | /// |
110 | | /// Updated TOML text. |
111 | | /// |
112 | | /// # Errors |
113 | | /// |
114 | | /// Returns an error if `changelogging_content` cannot be parsed as TOML. |
115 | 4 | pub fn set_changelogging_version(changelogging_content: &str, version: &str) -> Result<String> { |
116 | 4 | let mut doc: toml_edit::Document = changelogging_content |
117 | 4 | .parse() |
118 | 4 | .context("failed to parse changelogging.toml")?0 ; |
119 | 4 | doc["context"]["version"] = toml_edit::value(version); |
120 | 4 | Ok(doc.to_string()) |
121 | 4 | } |
122 | | |
123 | | /// Generate the changelog for the version currently recorded in `Cargo.toml`. |
124 | | /// |
125 | | /// 1. Reads the version from `Cargo.toml`. |
126 | | /// 2. Rewrites `changelogging.toml` with the new version. |
127 | | /// 3. Runs `changelogging build --remove`. |
128 | | /// |
129 | | /// # Arguments |
130 | | /// |
131 | | /// * `system` - Injected I/O provider. |
132 | | /// |
133 | | /// # Errors |
134 | | /// |
135 | | /// Returns an error if any step fails. |
136 | 2 | pub fn generate_changelog<S: ChangelogSystem>(system: &S) -> Result<()> { |
137 | 2 | let cargo_toml = system.read_cargo_toml()?0 ; |
138 | 2 | let version = extract_version_from_cargo_toml(&cargo_toml)?0 ; |
139 | 2 | println!("Generating changelog for version {version}"); |
140 | | |
141 | 2 | let changelogging_toml = system.read_changelogging_toml()?0 ; |
142 | 2 | let updated = set_changelogging_version(&changelogging_toml, &version)?0 ; |
143 | 2 | system.write_changelogging_toml(&updated)?0 ; |
144 | | |
145 | 2 | system.run_changelogging_build()?0 ; |
146 | 2 | Ok(()) |
147 | 2 | } |
148 | | |
149 | | #[cfg(test)] |
150 | | #[path = "tests/test_changelog.rs"] |
151 | | mod tests; |